module Wasp.Analyzer.Parser.AbstractParser.Monad
  ( ParseState (..),
    pstatePos,
    ParserM,
    runParserM,
    consume,
    advance,
    unexpectedNode,
    throwMissingSyntax,
    try,
  )
where

import Control.Monad.Except (Except, catchError, runExcept, throwError)
import Control.Monad.State.Strict (StateT, gets, modify, runStateT)
import Wasp.Analyzer.Parser.CST (SyntaxKind)
import Wasp.Analyzer.Parser.ParseError
import Wasp.Analyzer.Parser.SourcePosition (SourcePosition (SourcePosition))

data ParseState = ParseState
  { pstateLine :: !Int,
    pstateColumn :: !Int,
    pstateLastLineLength :: !Int,
    pstateRemainingSource :: String,
    pstateErrors :: [ParseError]
  }
  deriving (Eq, Show)

pstatePos :: ParseState -> SourcePosition
pstatePos s = SourcePosition (pstateLine s) (pstateColumn s)

type ParserM a = StateT ParseState (Except ParseError) a

runParserM :: String -> ParserM a -> Either [ParseError] a
runParserM source parser = case runExcept $ runStateT parser initialState of
  Left err -> Left [err]
  Right (result, finalState) ->
    if not (null $ pstateErrors finalState)
      then Left $ pstateErrors finalState
      else Right result
  where
    initialState =
      ParseState
        { pstateLine = 1,
          pstateColumn = 1,
          pstateLastLineLength = 1,
          pstateRemainingSource = source,
          pstateErrors = []
        }

consume :: Int -> ParserM String
consume amount = do
  lexeme <- gets (take amount . pstateRemainingSource)
  advance amount
  return lexeme

advance :: Int -> ParserM ()
advance 0 = return ()
advance amount = do
  gets (head . pstateRemainingSource) >>= \case
    '\n' -> modify (\s -> s {pstateLine = pstateLine s + 1, pstateColumn = 1, pstateLastLineLength = pstateColumn s})
    _ -> modify (\s -> s {pstateColumn = pstateColumn s + 1})
  modify (\s -> s {pstateRemainingSource = tail (pstateRemainingSource s)})
  advance (amount - 1)

-- | Returns a GHC error. Use this when a node is found in the CST that should
-- not be in that position. This scenario is a bug in the parser, which is why
-- it crashes waspc.
unexpectedNode :: SyntaxKind -> String -> ParserM a
unexpectedNode unexpectedKind locationDescription =
  error $ "Unexpected syntax " ++ show unexpectedKind ++ " " ++ locationDescription ++ " created by CST"

throwMissingSyntax :: String -> ParserM a
throwMissingSyntax reason = do
  pos <- SourcePosition <$> gets pstateLine <*> gets pstateColumn
  throwError $ MissingSyntax pos reason

-- | @try parser@ runs @parser@ and
-- * When @parser@ succeeds, returns the result of the parser
-- * When @parser@ fails, appends the error generated by it to the error list
--   in the state, returns that error, and resets the state to how it was before
--   running @parser@.
--
-- ==== __Example__
-- >>> import Control.Arrow (right)
-- >>> let initialState = ParseState 1 1 1 "test" []
-- >>> right fst $ runExcept $ flip runStateT initialState $ advance 2 >> try (advance 1 >> throwError undefined) >> gets pstateRemainingSource
-- Right "st"
--
-- ==== __Example__
-- >>> right fst $ runExcept $ flip runStateT initialState $ advance 2 >> try (advance 1) >> gets pstateRemainingSource
-- Right "t"
try :: ParserM a -> ParserM (Either ParseError a)
try parser =
  fmap Right parser
    `catchError` ( \err -> do
                     modify (\s -> s {pstateErrors = pstateErrors s ++ [err]})
                     pure $ Left err
                 )
